import os
import json

from os import path

from cpbox.tool import file
from cpbox.tool import utils
from cpbox.tool import http

from cpapp.configman import ConfigMan
from cpapp.patch import Patcher
from cpapp.market import Market
from cpapp.oss import AliyunOss
from cpapp.repr import ReprAble
from cpapp import version

URL_PRE = 'http://cp-prod-public.yizhoucp.cn/api/apps/zero/upgrade/android'
URL_SANDBOX = 'http://www-sandbox-316.cp-test.yizhoucp.cn/api/apps/zero/upgrade/android'

CHANNEL_PUSH_TYPE_MAP = {
    'huawei': 'huawei',
    'xiaomi': 'xiaomi',
    'meizu': 'meizu',
    'google': 'fcm',
    'oppo': 'oppo',
    'vivo': 'vivo',
}

DEFAULT_PUSH_TYPE = 'xiaomi'
DEFAULT_CHANNEL = 'default'
DEDICATED_PUSH_BUILD_CHANNELS = ['huawei', 'meizu', 'google', 'oppo', 'vivo']

BUILD_TYPE_DEBUG = 'debug'
BUILD_TYPE_RELEASE = 'release'


class Artifact(ReprAble):

    def __init__(self, build_task, env, channel):
        self.build_task = build_task
        self.channel = channel

        self.context = build_task.context
        self.apk = Apk(self.context, env, channel)
        self.released_apk = None
        self.apk_patch = None

        # self._try_to_find_patch()

    def _try_to_find_patch(self):
        if not self.context.build_info_provider.should_patch_for_env():
            self.context.app.logger.warn('no patch available')
            return
        last_release_info = self.context.build_info_provider.get_last_release_info()
        last_release_version_code = last_release_info.get('version_code', False)

        if self.channel in last_release_info.get('ext', {}):
            last_channel_release_info = last_release_info['ext'][self.channel]
            self.context.app.logger.info('find last_channel_release_info: %s', last_channel_release_info)
            self.released_apk = ReleasedApk(self.context, last_channel_release_info['channel_apk_path'],
                                            last_release_version_code)
            self.apk_patch = ApkPatch(self.context, self.apk, self.released_apk)
        else:
            self.context.app.logger.info('no last channel release found')

    def to_api_payload(self):
        channel_ext = {}
        channel_ext['channel_apk_path'] = self.apk.get_oss_path()
        channel_ext['channel_apk_md5'] = self.apk.get_file_md5()

        if self.apk_patch and self.released_apk:
            channel_ext['patch_file_path'] = self.apk_patch.get_oss_path()
            channel_ext['patch_file_md5'] = self.apk_patch.get_file_md5()
            channel_ext['patch_base_apk_md5'] = self.released_apk.get_file_md5()
            channel_ext['patch_base_apk_version_code'] = self.released_apk.version_code

        data = {}
        data['version_code'] = self.build_task.build_request.build_info_provider.get_version_code()
        data['build_type'] = self.build_task.build_request.build_type
        data['channel'] = self.channel
        data['channel_ext'] = json.dumps(channel_ext)
        return data

    def post_process(self):
        self.apk.move_to_local_storage()
        self.apk.upload_if_not_exist()
        self.apk.upload_default_apk_to_pgyer()
        if self.apk_patch is not None:
            self.apk_patch.patch()
            self.apk_patch.upload_if_not_exist()
        # self.context.build_info_provider.update_android_channel_build(self)


class LocalRemoteFile(ReprAble):
    __repr_exclude_keys__ = ['context']

    def __init__(self, context):
        self.context = context
        self._calculate()

    def _calculate(self):
        self.local_filepath = self.get_local_filepath()
        self.oss_path = self.get_oss_path()

    def get_file_md5(self):
        return file.md5_file(self.get_local_filepath())

    def get_local_filepath(self):
        return path.join(self.context.local_storage_dir, self.get_filename())

    def get_filename(self):
        pass

    def get_oss_path(self):
        return path.join(self.context.oss_base_path, self.get_filename())

    def upload_if_not_exist(self):
        self.context.oss.upload_if_not_exist(self.get_local_filepath(), self.get_oss_path())

    def download_if_not_exist(self):
        self.context.oss.download_if_not_exist(self.get_oss_path(), self.get_local_filepath())


class Apk(LocalRemoteFile):

    def __init__(self, context, env, channel):
        self.env = env
        self.channel = channel
        LocalRemoteFile.__init__(self, context)
        self.build_artifact_filepath = self.get_build_artifact_filepath()

    def get_build_artifact_filepath(self):
        return path.join(self.context.build_artifacts_dir, self.get_filename())

    def get_filename(self):
        return self.context.build_task.build_apk_name(self.channel) + '.apk'

    def move_to_local_storage(self):
        if os.path.isfile(self.build_artifact_filepath):
            cmd = 'mv %s %s' % (self.build_artifact_filepath, self.local_filepath)
            self.context.app.shell_run(cmd, dry_run=self.context.dry_run)

    def upload_default_apk_to_pgyer(self):
        if self.channel == DEFAULT_CHANNEL:
            market = Market(self)
            market.upload_to_pgyer(self.env, self.local_filepath)


class ReleasedApk(LocalRemoteFile):

    def __init__(self, context, oss_path, version_code):
        self.released_apk_oss_path = oss_path
        self.version_code = version_code
        LocalRemoteFile.__init__(self, context)

    def get_oss_path(self):
        return self.released_apk_oss_path

    def get_filename(self):
        return path.basename(self.released_apk_oss_path)


class ApkPatch(LocalRemoteFile):
    __repr_exclude_keys__ = ['apk', 'released_apk', 'context']

    def __init__(self, context, apk, released_apk):
        self.apk = apk
        self.released_apk = released_apk
        LocalRemoteFile.__init__(self, context)

    def get_filename(self):
        return '%s.from-v-%s.patch' % (self.apk.get_filename(), self.released_apk.version_code)

    def patch(self):
        self.released_apk.download_if_not_exist()
        self.context.patcher.bsdiff(self.released_apk.local_filepath, self.apk.local_filepath, self.local_filepath)


class BuildContext(ReprAble):
    __repr_keys__ = ['prj_root_dir', 'oss_base_path', 'build_artifacts_dir', 'local_storage_dir', 'dry_run']

    def __init__(self, builder, build_task):
        self.app = builder.app
        self.builder = builder
        self.dry_run = builder.dry_run
        self.build_task = build_task
        self.build_info_provider = build_task.build_request.build_info_provider
        self.patcher = Patcher(self.app)
        self._calculate_path()

    def _calculate_oss_base_path(self):
        provider = self.build_task.build_request.build_info_provider
        return 'apks/apks-%s/%s/v-%s' % (provider.env, provider.app_key, provider.get_version_code())

    def _calculate_prj_root_dir(self):
        build_request = self.build_task.build_request
        if self.builder.options['concurrent_build']:
            path_name = 'build-context/v-%s/build-by-push-%s' % (build_request.build_info_provider.get_version_code(), self.build_task.push_type)
            prj_root_dir = path.join(self.app.app_runtime_storage_dir, path_name)
            return prj_root_dir
        else:
            return self.builder.prj_root_dir

    def _calculate_path(self):
        self.prj_root_dir = self._calculate_prj_root_dir()
        self.oss_base_path = self._calculate_oss_base_path()

        self.build_artifacts_path = 'build-artifacts'

        self.build_artifacts_dir = path.join(self.prj_root_dir, self.build_artifacts_path)
        if self.build_info_provider.flavor is not None:
            self.build_artifacts_dir = path.join(self.build_artifacts_dir, self.build_info_provider.flavor)

        self.local_storage_dir = path.join(self.app.app_persistent_storage_dir, 'artifacts')

        self.app.logger.info('build_artifacts_dir is: %s', self.build_artifacts_dir)

        oss_config = self.app.read_app_config('app-config')['oss_config']

        self.oss = AliyunOss(self.app, oss_config, dry_run=self.dry_run)
        self.configman = ConfigMan(self.app, self.prj_root_dir, self.build_info_provider.flavor)

    def prepare(self):
        dry_run = self.dry_run
        if self.builder.options['concurrent_build']:
            if os.path.exists(self.prj_root_dir):
                file.rmtree(self.prj_root_dir)
            file.ensure_dir(self.prj_root_dir)
            cmd = 'cp -rf %s/* %s/' % (self.builder.prj_root_dir, self.prj_root_dir)
            self.app.shell_run(cmd, dry_run=dry_run)

        file.ensure_dir(self.local_storage_dir)
        file.ensure_dir(self.build_artifacts_dir)

        self.configman.update_project_config(self.build_info_provider, dry_run=dry_run)
        self.configman.enable_push_type(self.build_task.push_type, dry_run=dry_run)


class BuildTask(ReprAble):
    __repr_exclude_keys__ = ['context']

    def __init__(self, build_request, push_type, channels, builder):
        self.build_request = build_request
        self.push_type = push_type
        self.channels_for_push_type = channels
        self.context = BuildContext(builder, self)

    def get_build_cmd(self):
        build_context = self.context
        gradlew_cmd = self._get_gradlew_cmd()
        channels = ','.join(self.channels_for_push_type)
        build_log_path = self.context.build_artifacts_dir + '/build.log'
        file.ensure_dir_for_file(build_log_path)

        format = self.build_apk_name('${channel}')
        cmd = 'cd %s; ./gradlew clean %s -Dorg.gradle.daemon=true -Pchannels=%s -Poutput=%s -Pformat=\'%s\' --console=plain > %s' % (
            build_context.prj_root_dir, gradlew_cmd, channels, build_context.build_artifacts_path, format,
            build_log_path)
        return cmd

    def get_artifact_list(self):
        list = []
        for channel in self.channels_for_push_type:
            env = self.build_request.build_info_provider.env
            artifact = Artifact(self, env, channel)
            list.append(artifact)
        return list

    def _get_gradlew_cmd(self):
        flavor = self.build_request.build_info_provider.flavor
        build_type = self.build_request.build_type
        if flavor is None:
            return 'apk' + build_type.capitalize()
        else:
            return 'apk' + flavor.capitalize() + build_type.capitalize()

    def build_apk_name(self, channel):
        provider = self.build_request.build_info_provider
        name = provider.app_key
        if provider.flavor is not None:
            name += '_flavor_%s' % (provider.flavor)
        name += '_channel_%s_push_%s_%s_%s' % (
            channel, self.push_type, provider.get_version_code(), provider.get_full_version_name())
        return name

    def run(self):
        self.context.prepare()
        cmd = self.get_build_cmd()
        self.context.app.shell_run(cmd, dry_run=self.context.dry_run)


class BuildRequest(ReprAble):
    __repr_exclude_keys__ = ['build_info_provider']

    def __init__(self, build_info_provider, build_type=BUILD_TYPE_DEBUG,
                 channels=[DEFAULT_CHANNEL]):
        self.build_info_provider = build_info_provider
        self.build_type = build_type
        self.channels = channels
        self.channels.append(DEFAULT_CHANNEL)

    def generate_build_task_list(self, builder):
        dedicated_push_build_channels = tuple(set(DEDICATED_PUSH_BUILD_CHANNELS) & set(self.channels))
        default_push_build_channels = tuple(set(self.channels) - set(DEDICATED_PUSH_BUILD_CHANNELS))

        build_list = []

        for channel in dedicated_push_build_channels:
            push_type = CHANNEL_PUSH_TYPE_MAP[channel]
            build_list.append(BuildTask(self, push_type, [channel], builder))

        if default_push_build_channels:
            push_type = DEFAULT_PUSH_TYPE
            build_list.append(BuildTask(self, push_type, default_push_build_channels, builder))

        return build_list


class BuildInfoProvider(ReprAble):
    __repr_exclude_keys__ = ['app', 'last_release_info']

    def __init__(self, app, env, app_key, flavor=None):
        self.app = app
        self.env = env
        self.app_key = app_key
        self.flavor = flavor
        self.last_release_info = None
        self.version_info = None

    def draft_new_version(self, version_name, version_code, author=False):
        url = self._get_base_url() + '/draft-new-version'
        params = {'app_key': self.app_key, 'version_name': version_name, 'author': author, 'version_code': version_code}
        http.get(url, params=params)

    def get_current_version_info(self):
        url = self._get_base_url() + '/get-current-version-info'
        params = {'app_key': self.app_key}
        ret = http.get(url, params=params)
        result = http.fetch_json(url, ret)
        raw = result['data']
        return raw

    def set_version_info(self, version_name=None, version_code=0):
        if version_code and version_name:
            self.version_info = version.VersionInfo(self.env, version_name, version_code)

    def get_version_info(self):
        return self.version_info

    def get_version_code(self):
        return self.version_info.version_code

    def get_full_version_name(self):
        return self.version_info.full_version_name

    def _get_base_url(self):
        if self.env == 'sandbox':
            return URL_SANDBOX
        return URL_PRE

    def should_patch_for_env(self):
        return self.scope_for_env() is not None

    def scope_for_env(self):
        if 'dev' in self.env:
            return 'dev'
        if 'sandbox' in self.env:
            return 'dev'
        if self.env == 'pre' or self.env == 'prod':
            return 'public'
        return None

    def get_last_release_info(self):
        if not self.last_release_info:
            url = self._get_base_url() + '/get-last-release-info'
            scope = self.scope_for_env()
            params = {'app_key': self.app_key, 'scope': scope}
            ret = http.get(url, params=params)
            result = http.fetch_json(url, ret)
            self.last_release_info = {}
            if result['data']:
                self.last_release_info = result['data']
        return self.last_release_info

    def update_android_channel_build(self, artifact):
        url = self._get_base_url() + '/update-build-channel-info'
        env_map = self.app.read_app_config('app-config')['jni_config']['env_key']
        env = env_map[self.env]

        data = {}
        data['app_key'] = self.app_key
        data['env'] = env
        data.update(artifact.to_api_payload())

        self.app.logger.info('update_android_channel_build data: %s', data)

        ret = http.post(url, data=data)
        result = http.fetch_json(url, ret)
        if result.get('code') != 0:
            self.app.logger.error('update_android_channel_build error: %s', ret)

    def get_build_info(self, version_code):
        url = self._get_base_url() + '/get-build-info-list'
        data = {}
        data['app_key'] = self.app_key
        ret = http.post(url, data=data)
        result = http.fetch_json(url, ret)
        for build_info in result['data']:
            if build_info['version_code'] == int(version_code):
                return build_info

    def get_apk_path(self, channel, version_code):
        build_info = self.get_build_info(version_code)
        if build_info['version_code'] == int(version_code):
            if channel in build_info['ext']:
                return build_info['ext'][channel]['channel_apk_path']
            else:
                return None
        return False

    def release_build(self, version_code, scope, release_note, developer_name):
        data = {}
        data['app_key'] = self.app_key
        data['version_code'] = version_code
        data['scope'] = scope
        data['release_note'] = release_note
        data['developer_name'] = developer_name
        url = self._get_base_url() + '/release-build'
        ret = http.post(url, data=data)
        result = http.fetch_json(url, ret)
        print(result)
